/**
 * ProtoTree Version 0.0.1-BETA
 * 
 * (c) Remo Fritzsche
 * 
 * @version $Id: ProtoTree.js 19535 2009-08-20 08:45:20Z rfritzsc $
 */
var ProtoTree = Class.create(
{
	// ProtoTree Version
	version: '0.0.1-BETA',
	
	// The tree model object
	model: null,
	
	domNodes: null,
	
	nodeTypes: null,
	
	STATE_COLLAPSED: 0,
	
	STATE_EXPANDED: 1,
	
	selectedNode: null,
	
	transfer: null,
	
	rootNodes: null,
	
	events: null,
	
	stack: null,
	
	// Node state
	expandedNodes: null,
	
	/**
	 * Initialization function, constructor
	 */
	initialize: function( initialModel, containerNode, nodeTypes, resourceBasePath, expandedNodes, selectedNode )
	{
		this.model = new Array();
		this.domNodes = {};
		this.domNodes.treeNodes = [];
		this.domNodes.containerNode = containerNode;
		this.resourceBasePath = resourceBasePath;
		this.expandedNodes = new Array();
		this.nodeTypes = nodeTypes;
		this.transfer = new Array();
		this.events = new Element('div');
		eventsHolderDiv = new Element('div');
		eventsHolderDiv.appendChild(this.events);
		this.stack = new Array();
		this.stack.nodesToExpand = new Array();
		this.transfer.initiallySelectedNode = selectedNode;

		/**
		 * Build initial structure for the model
		 */
		for ( var i = 0; i < nodeTypes.length; i++ )
		{
			this.model[ nodeTypes[ i ].name ] = new Array(); 
		}
		
		/**
		 * Remap initial model to internal model structure
		 */
		for( var i = 0; i < initialModel.length; i++ )
		{
			this.model[ initialModel[i].type ][ initialModel[i].id ] = initialModel[i];
			
			// Create structure for children array
			initialModel[i].children = new Array();
			
			for ( var j = 0; j < this.nodeTypes.length; j++ )
			{
				initialModel[i].children[ this.nodeTypes[ j ].name ] = new Array();
			}
		}
		
		/**
		 * Loop through initial model again and map children where applicable
		 */
		var rootNodes = new Array();
		
		for( var i = 0; i < initialModel.length; i++ )
		{
			node = initialModel[ i ];
			
			if ( node.parentId != null )
			{
				if ( typeof( this.model[ node.parentType ][ node.parentId ] ) != 'undefined' )
				{					
					parent = this.model[ node.parentType ][ node.parentId ];
					node.parent = parent;
					parent.children[ node.type ][ node.id ] = node;
				}
			}
			else
			{
				rootNodes[ rootNodes.length ] = node;
			}
		}
		
		/**
		 * Build initial structure for the array of expanded nodes
		 */
		for ( var i = 0; i < nodeTypes.length; i++ )
		{
			this.expandedNodes[ nodeTypes[ i ].name ] = []; 
		}

		/**
		 * Fill the array of expanded nodes with the initial set (if given)
		 */
		if ( expandedNodes )
		{
			for ( var i = 0; i < expandedNodes.length; i++ )
			{
				this.expandedNodes[ expandedNodes[ i ].type ][ expandedNodes[ i ].id ] = true;
			}
		}
		
		this._buildInitialTree( rootNodes, initialModel, selectedNode );
	},
	
	_buildInitialTree: function( rootNodes, initialModel, selectedNode )
	{
		// Create protoTree div node
		this.domNodes.protoTree = new Element( 'div' ).addClassName( 'protoTree' );
		this.domNodes.containerNode.appendChild( this.domNodes.protoTree );
		
		// Create root nodes		
		for ( i = 0; i < rootNodes.length; i++ )
		{
			var item = rootNodes[ i ];
			
			item.state = this.STATE_COLLAPSED;
			
			this.domNodes.protoTree.appendChild( this._createDomTreeNode( item ) );
		}
		
		/**
		 * Go through every node which is available yet, check if parent is expanded, and
		 * if not, change state to collapsed
		 */
		for ( var i=0; i < initialModel.length; i++ )
		{
			if ( initialModel[ i ].parent != null && initialModel[ i ].parent.state != this.STATE_EXPANDED )
			{
				initialModel[ i ].state = this.STATE_COLLAPSED;
			}
		}
		
		/**
		 * Save root nodes for further user
		 */
		this.rootNodes = rootNodes;
		
		this.events.observe( '_expandNodeFromStack:endOfChain', function() {
			if ( this.transfer.initiallySelectedNode != undefined )
			{
				this.selectNode( this.transfer.initiallySelectedNode.type, this.transfer.initiallySelectedNode.id );
			}
		}.bind(this));
		
		/**
		 * Expand nodes from stack
		 */
		this._expandNodeFromStack();
	},
	
	_updateExpando: function( item )
	{
		/**
		 * Remove potential event listener
		 */
		item.domNodes.expando.stopObserving( 'click' );
		
		/**
		 * Register event listener if it's not a leaf
		 */
		if ( ! item.isLeaf )
		{
			item.domNodes.expando.observe( 'click', this._onExpandoClick.bind( this ) );
		}
		
		/**
		 * Remove class names reflecting states
		 */
		if ( item.domNodes.expando.hasClassName( 'protoTree-node-expando-leaf' ) )
		{
			item.domNodes.expando.removeClassName( 'protoTree-node-expando-leaf' );
		}
		
		if ( item.domNodes.expando.hasClassName( 'protoTree-node-expando-expanded' ) )
		{
			item.domNodes.expando.removeClassName( 'protoTree-node-expando-expanded' );
		}
		
		if ( item.domNodes.expando.hasClassName( 'protoTree-node-expando-collapsed' ) )
		{
			item.domNodes.expando.removeClassName( 'protoTree-node-expando-collapsed' );
		}
		
		/**
		 * Apply correct classes
		 */
		if ( item.isLeaf )
		{
			item.domNodes.expando.addClassName( 'protoTree-node-expando-leaf' );
		}
		else if ( item.state == this.STATE_EXPANDED )
		{
			item.domNodes.expando.addClassName( 'protoTree-node-expando-expanded' );
		}
		else if ( item.state == this.STATE_COLLAPSED )
		{
			item.domNodes.expando.addClassName( 'protoTree-node-expando-collapsed' );
		}
		
		this.events.fire( 'after:_updateExpando', item );
	},
	
	_createDomTreeNode: function( item, isLast )
	{		
		var storage = {};
		
		// Node element
		storage.node = new Element( 'div' ).addClassName( 'protoTree-node' );
		
		if ( item.parentId == null )
		{
			storage.node.addClassName( 'protoTree-node-root' );
		}
		
		if ( isLast )
		{
			storage.node.addClassName( 'protoTree-node-last' );
		}
		
		// Expando
		storage.expando = new Element( 'img' ).addClassName( 'protoTree-node-expando' );
		storage.expando.src = this.resourceBasePath + 'blank.gif';
		
		// Check if its expanded or leaf
		if ( item.state == this.STATE_EXPANDED )
		{
			storage.expando.addClassName( 'protoTree-node-expando-expanded' );
		}
		else if ( item.isLeaf )
		{
			storage.expando.addClassName( 'protoTree-node-expando-leaf' );
		}
		
		storage.node.appendChild( storage.expando );
		
		// Icon
		storage.icon = new Element( 'img' ).addClassName( 'protoTree-node-icon' );
		storage.icon.src = this.resourceBasePath + 'blank.gif';
		
		storage.icon.currentIconClassName = this._getIconClass( item );
		storage.icon.addClassName( storage.icon.currentIconClassName );
		
		storage.node.appendChild( storage.icon );
		
		// Label
		storage.label = new Element( 'span' ).addClassName( 'protoTree-node-label' );
		storage.label.appendChild( document.createTextNode( item.label ) );
		storage.node.appendChild( storage.label );
		
		this.domNodes.treeNodes[ this.domNodes.treeNodes.length ] = storage;
		
		item.domNodes = this.domNodes.treeNodes[ this.domNodes.treeNodes.length - 1 ];
		
		// Children container
		storage.childrenContainer = new Element('div');
		storage.node.appendChild( storage.childrenContainer );
		
		// Attach item to main node
		storage.node.item = item;
		
		/**
		 * Append event listeners
		 */
		if ( ! item.isLeaf )
		{
			storage.expando.observe( 'click', this._onExpandoClick.bind( this ) );
		}
		
		storage.label.observe( 'click', this._onLabelClick.bind( this ) );
		
		/**
		 * Expand node if required to
		 */
		if ( typeof( this.expandedNodes[ item.type ][ item.id ] ) != 'undefined' )
		{
			this.stack.nodesToExpand[ this.stack.nodesToExpand.length ] = item;
			//this._expandNode( item );
		}
		
		this.model[ item.type ][ item.id ] = item;
		
		/**
		 * Execute callback
		 */
		this._afterNodeCreation( item );
		
		this.events.fire( 'after:_createDomTreeNode', item );
		this.events.fire( '_createDomTreeNode:' + item.type + ':' + item.id, item );
				
		return storage.node;
	},
	
	/**
	 * Takes the last recent node from node expansion stack, expands
	 * it and then fires the _expandNodeFromStack(item) event
	 */
	_expandNodeFromStack: function()
	{
		if ( this.stack.nodesToExpand[ 0 ] != undefined )
		{
			// Fetch node
			node = this.stack.nodesToExpand[ 0 ];
			
			// Rebuild / shift nodesToExpand array
			var nodesToExpandCopy = new Array();
			
			for ( var i = 1; i < this.stack.nodesToExpand.length; i++ )
			{
				nodesToExpandCopy[ i - 1 ] = this.stack.nodesToExpand[ i ];
			}
			
			this.stack.nodesToExpand = nodesToExpandCopy;
			
			// Observe completion of expanding
			this.events.observe( '_expandNode:' + node.type + ':' + node.id, function( event )
			{
				this._expandNodeFromStack();
				this.events.fire( '_expandNodeFromStack:' + event.memo.type + ':' + event.memo.id, event.memo  );
			}.bind(this));
			
			this._expandNode( node );
		}
		else
		{
			this.events.fire( '_expandNodeFromStack:' + node.type + ':' + node.id, node  );
			this.events.fire( '_expandNodeFromStack:endOfChain', node );
			return false;
		}
	},
	
	_expandNode: function ( node )
	{
		
		/**
		 * Check if node is leaf and if so, exit
		 */
		if ( node.isLeaf )
		{
			return false;
		}
		
		/**
		 * Collect children
		 */
		var children = this.model[ node.type ][ node.id ].children;
		
		/**
		 * Register node as expanded
		 */
		this.expandedNodes[ node.type ][ node.id ] = true;
		
		/**
		 * Change item state to expanded
		 */
		node.state = this.STATE_EXPANDED;
		
		/**
		 * Count children
		 */
		var childrenCount = 0;
		
		for ( var i = 0; i < this.nodeTypes.length; i++ )
		{
			if ( typeof( children[ this.nodeTypes[i].name ] ) != 'undefined' )
			{
				childrenCount += children[ this.nodeTypes[i].name ].length;
			}
		}
		
		/**
		 * Observe onChildrenChange:type:id
		 */
		this.events.observe( 'onChildrenChange:' + node.type + ':' + node.id, function( event )
		{
			// Fire completion event
			this.events.fire( '_expandNode:' + event.memo.type + ':' + event.memo.id, event.memo );
		}.bind(this));
		
		/**
		 * If we have no children, request them
		 */
		if ( childrenCount == 0 )
		{
			this._loadChildren( node, this.onChildrenChange.bind( this ) );
		}
		else
		{			
			/**
			 * Remap children
			 */
			var newChildren = new Array();
			
			this.transfer._expandNode = {};
			this.transfer._expandNode.newChildren = newChildren;
			
			// Iterate through node types
			for ( var i = 0; i < this.nodeTypes.length; i++ )
			{
				if ( typeof( children[ this.nodeTypes[i].name ] ) != 'undefined' )
				{
					
					for ( var j in children[ this.nodeTypes[i].name ] )
					{
						if ( typeof( children[ this.nodeTypes[i].name ][ j ] ) == 'object' )
						{
							this.transfer._expandNode.newChildren[ this.transfer._expandNode.newChildren.length ] = children[ this.nodeTypes[i].name ][ j ];
						}
					}
					
					// Iterate through children of this node type
					/*children[ this.nodeTypes[i].name ].each( function( item ) {
						this.transfer._expandNode.newChildren[ this.transfer._expandNode.newChildren.length ] = item;
					}, this );*/
				}
			}
			
			this.onChildrenChange( node, newChildren );
		}
		
		
	},
	
	reloadNodeChildren: function( node )
	{
		this._loadChildren( node , this.onChildrenChange.bind( this ) );
	},
	
	reloadRootNodes: function()
	{	
		/**
		 * Backup model
		 */
		this.transfer.reloadRootNodes = [];
		this.transfer.reloadRootNodes.modelBackup = [];
		
		for ( var i = 0; i < nodeTypes.length; i++ )
		{
			this.transfer.reloadRootNodes.modelBackup[ nodeTypes[ i ].name ] = new Array(); 
		}
		
		this.model.each( function( node )
		{
			this.transfer.reloadRootNodes.modelBackup[ node.type ][ node.id ] = node;
		}, this );
		
		/**
		 * Remove root nodes from model and then delete all the nodes in dom and model
		 */
		this.rootNodes.each( function( node )
		{
			delete( this.transfer.reloadRootNodes.modelBackup[ node.type ][ node.id ] );
			this.removeNode( node );
		}, this );
		
		/**
		 * Reinsert backup
		 */
		this.model = this.transfer.reloadRootNodes;
		
		/**
		 * Load new root nodes
		 */
		this._loadRootNodes( this._afterRootNodesLoaded.bind( this ) );
	},
	
	_afterRootNodesLoaded: function( nodes )
	{
		this.transfer._afterRootNodesLoaded = [];
		this.transfer._afterRootNodesLoaded.iterator = 0;
		this.transfer._afterRootNodesLoaded.nodeCount = nodes.length;
		
		nodes.each( function( node )
		{
			/**
			 * Add to model
			 */
			this.model[ node.type ][ node.id ] = node;
			
			/**
			 * Create and append dom node
			 */
			var domNode = this._createDomTreeNode( node , ( this.transfer._afterRootNodesLoaded.iterator == this.transfer._afterRootNodesLoaded.nodeCount - 1 ) );	
			this.domNodes.protoTree.appendChild( domNode );
			this.transfer._afterRootNodesLoaded.iterator++;
		}, this );
	},
	
	expandNode: function( nodeType, nodeId )
	{
		this._expandNode( this.model[ nodeType ][ nodeId ] );
	},
	
	onChildrenChange: function( /* item */ parent, /* item[] */ newChildren )
	{
		/**
		 * Change expando of parent node to expanded
		 */
		parent.domNodes.expando.addClassName( 'protoTree-node-expando-expanded' );
		
		/**
		 * Change icon class if required
		 */
		parent.domNodes.icon.removeClassName( parent.domNodes.icon.currentIconClassName );
		parent.domNodes.icon.currentIconClassName = this._getExpandedIconClass( parent );
		parent.domNodes.icon.addClassName( parent.domNodes.icon.currentIconClassName );
		
		/**
		 * Remove current child dom nodes
		 */
		parent.domNodes.childrenContainer.remove();
		parent.domNodes.childrenContainer = new Element('div');
		parent.domNodes.node.appendChild( parent.domNodes.childrenContainer );

		/**
		 * Remove current child model nodes
		 */
		var oldChildren = this.model[ parent.type ][ parent.id ].children;
		
		oldChildren.each( function( item ) {
			this.removeNode( item );
		}, this);
		
		this.model[ parent.type ][ parent.id ].children = new Array();
		
		/**
		 * Remove leaf state
		 */
		/*if ( parent.isLeaf )
		{
			parent.isLeaf = false;
			parent.domNodes.expando.removeClassName( 'protoTree-node-expando-leaf' );
			parent.domNodes.expando.addClassName( 'protoTree-node-expando-expanded' );
		}*/
		
		/**
		 * Append child nodes and add them to the model
		 */
		for( var i = 0; i < newChildren.length; i++ )
		{
			// Set children node state
			newChildren[ i ].state = this.STATE_COLLAPSED;
			
			// Set parent
			newChildren[ i ].parent = parent;
			
			// Set children node's children array to empty
			if ( typeof( newChildren[ i ].children ) == 'undefined' )
			{
				newChildren[ i ].children = new Array();
				
				// Initialize array
				for ( var j = 0; j < this.nodeTypes.length; j++ )
				{
					newChildren[ i ].children[ this.nodeTypes[ j ].name ] = new Array();
				}
			}
			
			// Add to dom node
			parent.domNodes.childrenContainer.appendChild( this._createDomTreeNode( newChildren[ i ], ( i == newChildren.length -1 ) ) );
			
			// Add to model
			this.model[ newChildren[ i ].type ][ newChildren[ i ].id ] = newChildren[ i ];
			
			if ( ! parent.children[ newChildren[ i ].type ]  )
			{
				parent.children[ newChildren[ i ].type ] = new Array();
			}
			
			parent.children[ newChildren[ i ].type ][ newChildren[ i ].id ] = newChildren[ i ];
		}
		
		/**
		 * If there are no new children, make parent to leaf
		 */
		parent.isLeaf = ( newChildren.length == 0 );
		
		parent.state = this.STATE_EXPANDED;
		
		this._updateExpando( parent );

		this._expandNodeFromStack();
		
		this.events.fire( 'onChildrenChange:' + parent.type + ':' + parent.id, parent );		
	},
	
	removeNode: function( node )
	{
		/**
		 * Remove children
		 */
		node.children.each( function( node )
		{
			this.removeNode( node );
		}, this );
		
		/**
		 * Remove dom node
		 */
		node.domNodes.node.remove();
		
		/**
		 * Remove node from model
		 */
		delete( this.model[ node.type ][ node.id ] );
		
		if ( node.parent != null )
		{
			delete( node.parent.children[ node.type ][ node.id ] );
		}
		
		/**
		 * Check if parent is now leaf
		 */
		if ( typeof( node.parent ) != 'undefined' )
		{
			var hasChildren=false;
			
			for ( var i = 0; i < this.nodeTypes.length; i++ )
			{
				if ( typeof( node.parent.children[ this.nodeTypes[ i ].name ] ) != 'undefined' && this._getAssocItemCount(node.parent.children[ this.nodeTypes[ i ].name ]) > 0 )
				{
					hasChildren = true;
					break;
				}
			}
			
			if ( ! hasChildren )
			{
				node.parent.isLeaf = true;
				this._updateExpando( node.parent );
			}
		}
		
		/**
		 * Unset node object
		 */
		delete( node );
	},

	/**
	 * Can be called when a node (and only the node, not it's children) has been changed. Reloads
	 * - label
	 * - icon
	 * 
	 * Please note: Type and ID have to stay the same!
	 */
	updateNode: function( newItem, deep )
	{
		var item = null;
		
		/**
		 * Find (old) corresponding node
		 */
		item = this.model[ newItem.type ][ newItem.id ];

		/**
		 * Update icon
		 */
		item.domNodes.icon.removeClassName( item.domNodes.icon.currentIconClassName );
		
		if ( item.expanded )
		{
			item.domNodes.icon.currentIconClassName = this._getExpandedIconClass( newItem );
		}
		else
		{
			item.domNodes.icon.currentIconClassName = this._getIconClass( newItem );
		}
		
		item.domNodes.icon.addClassName( item.domNodes.icon.currentIconClassName );
		 
		/**
		 * Update label
		 */
		item.domNodes.label.removeChild( item.domNodes.label.firstChild );
		item.domNodes.label.appendChild( document.createTextNode( newItem.label ) );
	},

	findNode: function( nodeType, nodeId )
	{
		if ( typeof( this.model[ nodeType ][ nodeId ] ) != 'undefined' )
		{
			return this.model[ nodeType ][ nodeId ];
		}
		else
		{
			return null;
		}
	},
	
	collapseNode: function( nodeType, nodeId )
	{
		/**
		 * Find corresponding node
		 */
		var node = this.model[ nodeType ][ nodeId ];		
		
		// If not found, exit
		if ( ! node )
		{
			return false;
		}
		
		/**
		 * Check if node is leaf and if so, exit
		 */
		if ( node.isLeaf )
		{
			return false;
		}
		
		/**
		 * Change item state to collapsed
		 */
		node.state = this.STATE_COLLAPSED;
		
		/**
		 * Unregister node as expanded
		 */
		delete( this.expandedNodes[ nodeType ][ nodeId ] );
		
		/**
		 * Change expando of parent node to collapsed
		 */
		node.domNodes.expando.removeClassName( 'protoTree-node-expando-expanded' );
		
		/**
		 * Change icon class if required
		 */
		node.domNodes.icon.removeClassName( node.domNodes.icon.currentIconClassName );
		node.domNodes.icon.currentIconClassName = this._getIconClass( node );
		node.domNodes.icon.addClassName( node.domNodes.icon.currentIconClassName );
		
		/**
		 * Remove child nodes
		 */
		node.domNodes.childrenContainer.remove();
		node.domNodes.childrenContainer = new Element('div');
		node.domNodes.node.appendChild( node.domNodes.childrenContainer );
	},
	
	__reSelectNode: function( event, node )
	{	
		/**
		 * If there is a selected node, find and select it
		 */
		var node = this.findNode( node.type, node.id );
	
		this.events.stopObserving( '_createDomTreeNode:' + node.type + ':' + node.id, this.__reSelectNodeBound );
		this.selectNode( node.type, node.id );
	},
	
	selectNode: function( nodeType, nodeId )
	{	
		if ( this.model[ nodeType ][ nodeId ] == undefined || this.model[ nodeType ][ nodeId ].domNodes == undefined )
		{
			this.__reSelectNodeBound = this.__reSelectNode.bindAsEventListener( this, { type: nodeType, id: nodeId } );
			this.events.observe( '_createDomTreeNode:' + nodeType + ':' + nodeId, this.__reSelectNodeBound );
		}
		else
		{
			var node = this.model[ nodeType ][ nodeId ];
						
			if ( node )
			{
				/**
				 * Clear selection
				 */
				this.clearSelection();
				
				/**
				 * Set as selected node
				 */
				this.selectedNode = node;
				
				/**
				 * Add selected class
				 */
				node.domNodes.label.addClassName( 'protoTree-node-label-selected' );
			}
		}		
	},
	
	clearSelection: function()
	{
		if ( this.selectedNode != null  && this.selectedNode != undefined )
		{
			this.selectedNode.domNodes.label.removeClassName( 'protoTree-node-label-selected' );
			this.selectedNode = null;
		}
	},
	
	_onExpandoClick: function( event )
	{
		var item = Event.element(event).parentNode.item;
		
		if ( ! item.isLeaf && item.state == this.STATE_COLLAPSED )
		{
			this.expandNode( item.type, item.id );
		}
		else if ( ! item.isLeaf && item.state == this.STATE_EXPANDED )
		{
			this.collapseNode( item.type, item.id );
		}
	},
	
	_getAssocItemCount: function( array )
	{
		count = 0;
		
		for ( var i = 0; i < array.length; i++ )
		{
			if ( array[ i ] != undefined )
			{
				count++;
			}
		}
		
		return count;
	},

	// =======================================================================
	// Callbacks
	
	_onLabelClick: function( event )
	{
		var item = Event.element(event).parentNode.item;
		
		this.selectNode( item.type, item.id );
	},
	
	/**
	 * Overwrite this function to load children
	 */
	_loadChildren: function ( parentNode, onComplete )
	{
		onComplete( parentNode, [] );
	},
	
	_loadRootNodes: function ( onComplete )
	{
		onComplete( [] );
	},
	
	_getIconClass: function( item )
	{
		return '';
	},
	
	_getExpandedIconClass: function( item )
	{
		return '';
	},
	
	_afterNodeCreation: function( node )
	{
		
	}
});